﻿using System;
using System.Collections.Generic;
using TypeBuilderFx.Core;
using TypeBuilderFx.Core.Emitters;
using Simp.CodeModel.Exceptions;
using Simp.CodeModel.CoreFunctions.Sequences;
using Simp.CodeModel.CoreFunctions;

namespace Simp.CodeModel
{
	/// <summary>Contains methods helping to manipulate <see cref="IFunction"/>.</summary>
	public static class FunctionHelper
	{
		/// <summary>Factory of <see cref="IFunctionParameterBinding"/>.</summary>
		public static readonly IFactory<IFunctionParameterBinding, string, IFunction> FunctionParameterBindingFactory =
			(IFactory<IFunctionParameterBinding, string, IFunction>)ImmutablePropertyHolderEmitter.GetFactory<IFunctionParameterBinding>();

		/// <summary>Empty array of <see cref="string"/>.</summary>
		public static readonly string[] EmptyParameterNames = new string[0];

		/// <summary>Empty array of <see cref="IFunction"/>.</summary>
		public static readonly IFunction[] EmptyParameterValues = new IFunction[0];

		/// <summary>Empty array of <see cref="IFunctionParameterBinding"/>.</summary>
		public static readonly IFunctionParameterBinding[] EmptyBindings = new IFunctionParameterBinding[0];

		/// <summary>Returns the parameters to use to call a function.</summary>
		/// <remarks>Adds default parameters if needed.</remarks>
		/// <param name="function"></param>
		/// <param name="givenBindings"></param>
		/// <returns></returns>
		public static IFunction[] CreateParameters(IFunction function, IFunctionParameterBinding[] givenBindings)
		{
			if (function == null)
			{
				throw new ArgumentNullException("function");
			}
			if (givenBindings == null)
			{
				throw new ArgumentNullException("givenBindings");
			}
			if (givenBindings.Length < function.ParameterNames.Length - function.DefaultParameterValues.Length)
			{
				throw new ArgumentException("Missing parameters", "givenBindings");
			}
			if (givenBindings.Length > function.ParameterNames.Length)
			{
				throw new ArgumentException("Too many parameters", "givenBindings");
			}

			IFunction[] parameters = new IFunction[function.ParameterNames.Length];
			IFunctionParameterBinding[] defaultParameterValues = function.DefaultParameterValues;
			bool isUsingNamedParameters = false;

			//	Scan the parameters of the function to call, see if the parameters are given, otherwise, take default ones
			for (int i = 0; i != parameters.Length; ++i)
			{
				string parameterName = function.ParameterNames[i];

				if (!isUsingNamedParameters && i < givenBindings.Length && string.IsNullOrEmpty(givenBindings[i].Name))
				{	//	Using parameters in a nameless fashion
					parameters[i] = givenBindings[i].Value;
				}
				else
				{	//	Find named parameters
					isUsingNamedParameters = true;

					IFunctionParameterBinding foundBinding =
						Array.Find(givenBindings, delegate(IFunctionParameterBinding binding)
						{
							return binding.Name == parameterName;
						});

					if (foundBinding != null)
					{
						parameters[i] = foundBinding.Value;
					}
					else
					{	//	Find default parameter
						IFunctionParameterBinding foundDefaultBinding =
							Array.Find(defaultParameterValues, delegate(IFunctionParameterBinding binding)
							{
								return binding.Name == parameterName;
							});

						if (foundDefaultBinding != null)
						{
							parameters[i] = foundDefaultBinding.Value;
						}
						else
						{
							throw new ParameterBindingException(function.FunctionName, parameterName);
						}
					}
				}
			}

			return parameters;
		}

		/// <summary>Returns an index of functions.</summary>
		/// <param name="functionCollection"></param>
		/// <returns></returns>
		public static IDictionary<string, IFunction> IndexFunctionByName(IEnumerable<IFunction> functionCollection)
		{
			IDictionary<string, IFunction> index = new Dictionary<string, IFunction>();

			foreach (IFunction function in functionCollection)
			{
				index.Add(function.FunctionName, function);
			}

			return index;
		}

		/// <summary>
		/// Apply a simplification on a function and compensate if it returns <c>null</c> by returning the original
		/// function.
		/// </summary>
		/// <param name="simplificationMode"/>
		/// <param name="function"/>
		/// <param name="parameters"/>
		/// <returns></returns>
		public static IFunction ApplySimplification(
			SimplificationMode simplificationMode,
			IFunction function,
			params IFunction[] parameters)
		{
			if (function == null)
			{
				throw new ArgumentNullException("function");
			}

			IFunction simplifiedFunction = function.Simplify(simplificationMode, parameters);

			if (simplifiedFunction == null)
			{
				return function;
			}
			else
			{
				return simplifiedFunction;
			}
		}

		/// <summary>
		/// Have the visitor visit the function and compensate if it returns <c>null</c> by returning the original
		/// function.
		/// </summary>
		/// <param name="function"></param>
		/// <param name="visitor"></param>
		/// <returns></returns>
		public static IFunction ApplyVisitor(IFunction function, IFunctionVisitor visitor)
		{
			IFunction visitedFunction = visitor.Visit(function);

			if (visitedFunction == null)
			{
				return function;
			}
			else
			{
				return visitedFunction;
			}
		}

		/// <summary>Validates the parameters of a function, see if they match.</summary>
		/// <param name="function"></param>
		/// <param name="parameters"></param>
		public static void ValidateParameters(IFunction function, IFunction[] parameters)
		{
			if (parameters == null)
			{
				throw new ArgumentNullException("parameters");
			}
			else if (parameters.Length != function.ParameterNames.Length)
			{
				string message = string.Format(
					"Function '{0}' takes {1} parameters in input but has {2}",
					function.FunctionName,
					function.ParameterNames.Length,
					parameters.Length);

				throw new ArgumentNullException(message, "parameters");
			}
		}

		/// <summary>Serializes a <see cref="IFunction"/> into a <see cref="string"/>.</summary>
		/// <remarks>Uses <see cref="IFunctionSerializer"/> implementations.</remarks>
		/// <param name="function"></param>
		/// <param name="parameters"/>
		/// <returns></returns>
		public static string Serialize(IFunction function, params IFunction[] parameters)
		{
			if (function == null)
			{
				throw new ArgumentNullException("function");
			}

			AppendOnlyStringBuilder builder = new AppendOnlyStringBuilder();

			Serialize(function, builder, parameters);

			return builder.ToString();
		}

		/// <summary>Serializes a <see cref="IFunction"/> into a <see cref="string"/>.</summary>
		/// <remarks>Uses <see cref="IFunctionSerializer"/> implementations.</remarks>
		/// <param name="function"></param>
		/// <param name="builder"></param>
		public static void Serialize(IFunction function, AppendOnlyStringBuilder builder, params IFunction[] parameters)
		{
			if (function == null)
			{
				throw new ArgumentNullException("function");
			}
			if (builder == null)
			{
				throw new ArgumentNullException("builder");
			}
			if (parameters == null)
			{
				throw new ArgumentNullException("parameters");
			}

			IFunctionSerializer serializer = function as IFunctionSerializer;

			if (serializer != null)
			{
				serializer.Serialize(builder, parameters);
			}
			else
			{	//	Generic serialization
				builder.Append("<Unsupported : ");
				builder.Append(function.FunctionName);
				builder.Append(">");
			}
		}

		/// <summary>Returns a <see cref="ISequenceAccessor"/> to the underlying sequence if any (otherwise returns <c>null</c>).</summary>
		/// <remarks>Takes care of the logic of passing through bound function callings.</remarks>
		/// <param name="function"/>
		/// <param name="parameters"/>
		/// <returns></returns>
		public static ISequenceAccessor GetAccessor(IFunction function, params IFunction[] parameters)
		{
			BoundFunctionCalling boundFunctionCalling = function as BoundFunctionCalling;

			if (boundFunctionCalling == null)
			{
				return null;
			}
			else
			{
				ISequenceFunction sequence = boundFunctionCalling.InnerFunction as ISequenceFunction;

				if (sequence == null)
				{
					return null;
				}
				else
				{
					return sequence.GetAccessor(boundFunctionCalling.FunctionParameters);
				}
			}
		}

		/// <summary>Checks if the given function is a bound function with the give function type as inner function.</summary>
		/// <typeparam name="FT"></typeparam>
		/// <param name="function"></param>
		/// <returns></returns>
		public static BoundFunctionCalling TryGetBoundFunction<FT>(IFunction function)
			where FT : class, IFunction
		{
			FT innerFunction;

			return TryGetBoundFunction(function, out innerFunction);
		}

		/// <summary>Checks if the given function is a bound function with the give function type as inner function.</summary>
		/// <typeparam name="FT"></typeparam>
		/// <param name="function"></param>
		/// <param name="innerFunction"></param>
		/// <returns></returns>
		public static BoundFunctionCalling TryGetBoundFunction<FT>(IFunction function, out FT innerFunction)
			where FT : class, IFunction
		{
			BoundFunctionCalling boundFunctionCalling = function as BoundFunctionCalling;

			if (boundFunctionCalling == null)
			{
				innerFunction = null;

				return null;
			}
			else
			{
				innerFunction = boundFunctionCalling.InnerFunction as FT;

				if (innerFunction == null)
				{
					return null;
				}
				else
				{
					return boundFunctionCalling;
				}
			}
		}

		/// <summary>Tries to find a boolean value and returns it.  If the function isn't a boolean, it returns <c>null</c>.</summary>
		/// <param name="function"></param>
		/// <returns></returns>
		public static bool? TryGetBooleanValue(IFunction function)
		{
			if (function is TrueFunction)
			{
				return true;
			}
			else if (function is FalseFunction)
			{
				return false;
			}
			else
			{
				return null;
			}
		}
	}
}